函数工具调用

函数即工具

  • 在方法即工具中有一项限制是参数和返回类型不得是函数式类型(如 FunctionSupplierConsumer)。
  • SpringAI为函数式类型单独提供了构建工具的方式。

使用 FunctionToolCallback

  • 可以通过FunctionToolCallback将Java中的函数式类型(FunctionSupplierConsumerBiFunction)构建成工具。

  • 使用FunctionToolCallback.Builder来构建FunctionToolCallback实例。可以构建的属性如下:

    • String name:工具名称。
    • String description:工具描述。
    • Type inputType:函数输入类型。必须输入。如果是无输入参数的工具使用Void.class
    • String inputSchema:输入参数的Json Schema。这里非必须,默认按inputType生成。
    • ToolMetadata toolMetadata:额外配置,通过 ToolMetadata.Builder 类构建。
    • BiFunction<I, ToolContext, O> toolFunction:用于工具调用的函数类型对象。
    • ToolCallResultConverter toolCallResultConverter:工具调用结果的转换器。
  • 构建的属性整体与MethodToolCallback.Builder类似。下面模拟一个将美元兑换成RMB的工具案例。

    • 这里模拟一个RMB的单位,创建一个单位枚举,包含分/角/元。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      @Getter
      @AllArgsConstructor
      public enum Unit {

      @ToolParam(description = "分")
      F(100),

      @ToolParam(description = "角")
      J(10),

      @ToolParam(description = "元")
      Y(1)

      ;

      private int multiple;
      public BigDecimal getUnitMultiple() {
      return BigDecimal.valueOf(this.getMultiple());
      }

      }
    • 然后创建输入、输出的recode类。单位非必须项,默认:分。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      public record Request(@ToolParam(description = "需要转换成人民币的美元数量。") BigDecimal dollar,
      @ToolParam(required = false, description = "人民币单位。默认:分") Unit unit) {
      public Request {
      if(Objects.isNull(unit)) {
      unit = Unit.F;
      }
      }

      }

      public record Response(BigDecimal rmb) {

      }
    • 创建一个工具类实现Function<I, O>Request/Response是内部类。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @Slf4j
      public class FuncDollarToRmbTools implements Function<FuncDollarToRmbTools.Request, FuncDollarToRmbTools.Response> {
      private final BigDecimal rate = BigDecimal.valueOf(7.17);
      @Override
      public Response apply(Request request) {
      log.info("\n美元转换成人民币,参数 -> {}。", ConvertorUtils.toJsonString(request));
      return new Response(request.dollar()
      .multiply(rate)
      .multiply(request.unit().getUnitMultiple())
      .setScale(2));
      }
      }
    • 使用FunctionToolCallback.Builder构建实例

      1
      2
      3
      4
      5
      6
      7
      8
      FunctionToolCallback<FuncDollarToRmbTools.Request, FuncDollarToRmbTools.Response> toolCallback = FunctionToolCallback
      // 名称和Function工具对象。
      .builder("FuncDollarToRmbTools", new FuncDollarToRmbTools())
      // 描述
      .description("可以将美元转换成人名币的计算器,需要提供人名币数量,以及需要转换成的单位(默认:分)。")
      // 输入类型
      .inputType(FuncDollarToRmbTools.Request.class)
      .build();
    • 是用.toolCallbacks()添加工具。这里调用两次,一次不说明单位,一次指定单位。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      // 不说明单位
      String content = toolsClient.prompt()
      .user("15美元可以兑换多少人名币?")
      .toolCallbacks(toolCallback)
      .call()
      .content();
      log.info("\n[funcToolsExample] content -> \n{}", content);

      // 指定单位
      content = toolsClient.prompt()
      .user("15美元可以兑换多少角人名币?")
      .toolCallbacks(toolCallback)
      .call()
      .content();
      log.info("\n[funcToolsExample] unit(角) content -> \n{}", content);
    • 结果输出。两个都掉用了工具,指定单位时单位也换成了枚举中的J。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      2025-07-11T15:47:13.895+08:00  INFO 218 --- [spring-ai-example] [           main] c.s.a.e.tools.two.FuncDollarToRmbTools   : 
      美元转换成人民币,参数 -> {
      "dollar" : 15,
      "unit" : "F"
      }。
      2025-07-11T15:47:18.514+08:00 INFO 218 --- [spring-ai-example] [ main] c.s.a.e.tools.two.FuncToolsExample :
      [funcToolsExample] content ->
      15美元可以兑换107.55元人民币(以分为单位计算为10755分)。

      2025-07-11T15:47:23.503+08:00 INFO 218 --- [spring-ai-example] [ main] c.s.a.e.tools.two.FuncDollarToRmbTools :
      美元转换成人民币,参数 -> {
      "dollar" : 15,
      "unit" : "J"
      }。
      2025-07-11T15:47:29.850+08:00 INFO 218 --- [spring-ai-example] [ main] c.s.a.e.tools.two.FuncToolsExample :
      [funcToolsExample] unit(角) content ->
      15美元可以兑换1075.5角人民币。
  • 使用Supplier无输入参数的工具

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    String content = toolsClient.prompt()
    .user("下个月5号是星期几?")
    .toolCallbacks(FunctionToolCallback
    .builder("getDatetime", () -> LocalDateTime.now().toString())
    .description("可以获取当前时间。")
    // 使用 Void.class
    .inputType(Void.class)
    .build())
    .call()
    .content();

将工具定义成Spring Bean

  • 上面是通过手动构建将工具实例添加到ChatClient,也可以通过将工具定义为Spring Bean添加到ChatClient

    • 默认使用Bean的名称作为工具名称。
    • 可以通过@Description注解来配置工具描述。
  • 使用@Configuration的方式来定义Bean`,案例如下:

    • 这里定义两个工具Bean,一个Function,一个Supplier,且都使用的表达式编程。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      @Configuration
      public class BeanToolsConfiguration {

      @Bean
      @Description("将美元转换成人名币,需要提供美元数量及人民币单位。")
      public Function<FuncDollarToRmbTools.Request, FuncDollarToRmbTools.Response> beanRmbTools() {
      return request -> new FuncDollarToRmbTools.Response(request.dollar()
      .multiply(BigDecimal.valueOf(7.17))
      .multiply(request.unit().getUnitMultiple())
      .setScale(2));
      }

      @Bean
      @Description("获取当前时间。")
      public Supplier<String> beanDataTools() {
      return () -> LocalDateTime.now().toString();
      }
      }
    • 先看看Supplier的工具调用。添加工具使用.toolNames()添加Bean名称就可以了。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public void beanDataToolsExample() {
      String content = toolsClient.prompt()
      .user("下个月5号是星期几?")
      // 直接添加Bean(工具)名称就行
      .toolNames("beanDataTools")
      .call()
      .content();
      log.info("\n[simpleFuncToolsExample] content -> \n{}", content);
      }
    • 调用结果。这个肯定是获取了当前时间的,一般AI是无法知道当前时间的。

      1
      2
      3
      4
      5
      2025-07-11T16:08:15.276+08:00  INFO 716 --- [spring-ai-example] [           main] c.s.a.e.tools.two.BeanFuncToolsExample   : 
      [simpleFuncToolsExample] content ->
      当前时间是2025711日。下个月5号是202585日。我们需要计算202585日是星期几。

      202585日是星期二。
    • Function的工具调用

      1
      2
      3
      4
      5
      6
      7
      8
      public void beanRmbToolsExample() {
      String content = toolsClient.prompt()
      .user("33美元可以兑换多少元人名币?")
      .toolNames("beanRmbTools")
      .call()
      .content();
      log.info("\n[funcToolsExample] unit(元) content -> \n{}", content);
      }
    • 结果。是正常调用了的。

      1
      2
      3
      2025-07-11T16:11:12.065+08:00  INFO 794 --- [spring-ai-example] [           main] c.s.a.e.tools.two.BeanFuncToolsExample   : 
      [funcToolsExample] unit(元) content ->
      33美元可以兑换236.61元人民币。
    • 可以将方法即工具的Java实例对象定义成Bean,然后使用Bean Name添加工具吗?

      • 先说结论,是不可以的。

      • 因为Spring默认将从容器中取到的Bean构建成FunctionToolCallback。且如果不是函数式类型抛出异常。

        Bean构建ToolCallback

总结

  • 函数式工具使用上相对方便、快捷,特别是定义成bean的方式,配合使用表达式编程,是较为高效的一种方式。
  • 但是函数式工具的限制更多,不支持的类型相对更多:
    • 基本类型
    • 集合类型( ListMapArraySet
    • Optional
    • 异步类型(如 CompletableFutureFuture
    • 响应式类型(如 FlowMonoFlux
  • 不支持基本类型应该是函数式类型输入输出都是泛型,不支持基本类型,可以用包装类。
  • 集合类型类型不支持推测也是泛型无法识别集合中实际类型无法生成Schema,主要应该是定义成bean的方式。
  • 选择使用函数式工具还是方法即工具,可以从限制上来考虑,当然还要结合编程习惯以及团队的编程规范综合考虑。

最后

  • 不论基于方法的工具还是函数式编程,只是编程方式不同而已,其本质理论上还是一样的。
  • 下一篇继续学习记录工具调用SpringAI提供的一些特性。
  • 所有案例的源码,都会提交在GitHub上。包:com.spring.ai.example.tools.two